import utils from '../utils'
import {BufferReader} from '../io'
import shapeFile from './index.js'
import {ShpRecordClass} from './index.js'

function ShpReader(shpSrc, shxSrc) {
  if (this instanceof ShpReader === false) {
    return new ShpReader(shpSrc, shxSrc)
  }

  const shpFile = utils.isString(shpSrc) ? new FileReader(shpSrc) : new BufferReader(shpSrc)
  const header = parseHeader(shpFile.readToBinArray(0, 100))
  const shpSize = shpFile.size()
  const RecordClass = new ShpRecordClass(header.type)
  let shpOffset = null
  let recordCount = null
  let skippedBytes = null
  let shxBin = null
  let shxFile = null

  if (shxSrc) {
    shxFile = utils.isString(shxSrc) ? new FileReader(shxSrc) : new BufferReader(shxSrc)
    shxBin = shxFile.readToBinArray(0, shxFile.size()).bigEndian()
  }

  reset()

  this.header = function() {
    return header
  }

  // Callback interface: for each record in a .shp file, pass a
  //   record object to a callback function
  //
  this.forEachShape = function(callback) {
    let shape = this.nextShape()
    while (shape) {
      callback(shape)
      shape = this.nextShape()
    }
  }

  // Iterator interface for reading shape records
  this.nextShape = function() {
    const shape = readNextShape()
    if (!shape) {
      if (skippedBytes > 0) {
        // Encountered in files from natural earth v2.0.0:
        // ne_10m_admin_0_boundary_lines_land.shp
        // ne_110m_admin_0_scale_rank.shp
        // verbose("Skipped over " + skippedBytes + " non-data bytes in the .shp file.")
      }
      shpFile.close()
      reset()
    }
    return shape
  }

  function readNextShape() {
    const expectedId = recordCount + 1 // Shapefile ids are 1-based
    let offset = null
    if (done()) return null
    if (shxBin) {
      shxBin.position(100 + recordCount * 8)
      offset = shxBin.readUint32() * 2
      if (offset > shpOffset) {
        skippedBytes += offset - shpOffset
      }
    } else {
      offset = shpOffset
    }
    let shape = readShapeAtOffset(offset)
    if (!shape) {
      // Some in-the-wild .shp files contain junk bytes between records. This
      // is a problem if the .shx index file is not present.
      // Here, we try to scan past the junk to find the next record.
      shape = huntForNextShape(offset, expectedId)
    }
    if (shape) {
      if (shape.id < expectedId) {
        // message("Found a Shapefile record with the same id as a previous record (" + shape.id + ") -- skipping.")
        return readNextShape()
      } else if (shape.id > expectedId) {
        // stop("Shapefile contains an out-of-sequence record. Possible data corruption -- bailing.")
        utils.error('Shapefile contains an out-of-sequence record. Possible data corruption -- bailing.')
      }
      recordCount++
    }
    return shape || null
  }

  function done() {
    if (shxFile && shxFile.size() <= 100 + recordCount * 8) return true
    if (shpOffset + 12 > shpSize) return true
    return false
  }

  function reset() {
    shpOffset = 100
    skippedBytes = 0
    recordCount = 0
  }

  function parseHeader(bin) {
    const header = {
      signature: bin.bigEndian().readUint32(),
      byteLength: bin.skipBytes(20).readUint32() * 2,
      version: bin.littleEndian().readUint32(),
      type: bin.readUint32(),
      bounds: bin.readFloat64Array(4), // xmin, ymin, xmax, ymax
      zbounds: bin.readFloat64Array(2),
      mbounds: bin.readFloat64Array(2)
    }

    if (header.signature != 9994) {
      utils.error("Not a valid .shp file")
    }

    if (!shapeFile.isSupportedShapefileType(header.type)) {
      utils.error(`Unsupported .shp type:${header.type}`)
    }

    if (header.byteLength != shpFile.size()) {
      utils.error("File size of .shp doesn't match size in header")
    }

    return header
  }

  function readShapeAtOffset(offset) {
    let shape = null
    let bin = null
    let goodSize = null
    let goodType = null

    if (offset + 12 <= shpSize) {
      bin = shpFile.readToBinArray(offset, 12)
      const recordId = bin.bigEndian().readUint32()
      // record size is bytes in content section + 8 header bytes
      const recordSize = bin.readUint32() * 2 + 8
      const recordType = bin.littleEndian().readUint32()
      goodSize = offset + recordSize <= shpSize && recordSize >= 12
      goodType = recordType === 0 || recordType == header.type
      if (goodSize && goodType) {
        bin = shpFile.readToBinArray(offset, recordSize)
        shape = new RecordClass(bin, recordSize)
        shpOffset = offset + shape.byteLength // advance read position
      }
    }
    return shape
  }

  // TODO: add tests
  // Try to scan past unreadable content to find next record
  function huntForNextShape(start, id) {
    let offset = start + 4
    let shape = null
    let count = null
    while (offset + 12 <= shpSize) {
      const bin = shpFile.readToBinArray(offset, 12)
      const recordId = bin.bigEndian().readUint32()
      const recordType = bin.littleEndian().skipBytes(4).readUint32()
      if (recordId == id && (recordType == header.type || recordType === 0)) {
        // we have a likely position, but may still be unparsable
        shape = readShapeAtOffset(offset)
        break
      }
      offset += 4 // try next integer position
    }
    count = shape ? offset - start : shpSize - start
    // debug('Skipped', count, 'bytes', shape ? 'before record ' + id : 'at the end of the file');
    skippedBytes += count
    return shape
  }
}

ShpReader.prototype.type = function() {
  return this.header().type
}

ShpReader.prototype.getCounts = function() {
  const counts = {
    nullCount: 0,
    partCount: 0,
    shapeCount: 0,
    pointCount: 0
  }
  this.forEachShape(function(shp) {
    if (shp.isNull) counts.nullCount++
    counts.pointCount += shp.pointCount
    counts.partCount += shp.partCount
    counts.shapeCount++
  })
  return counts
}

export default ShpReader